#include <QDebug>
#include <QTimer>
#include <protoqtrpc/BlockingClosure.h>
#include <protoqtrpc/RpcController.h>
#include <protoqtrpc/SocketRpcChannel.h>
#include "protoqtrpc/location/LocationService.pb.h"
#include "LocationServiceClient.h"

namespace protoqtrpc {
namespace location {

class LocationServiceClientPrivate
{
public:
    BlockingClosure closure;
    RpcController controller;
    SocketRpcChannel channel;
    LocationService::Stub client;
    QTimer timerSession;

    LocationServiceClientPrivate(const QHostAddress& host, quint16 port):
        channel(host, port), client(&channel)
    {
    }
};

inline LocationServiceClient::ServerInfo& operator<<(
        LocationServiceClient::ServerInfo& outServerInfo,
        const ::protoqtrpc::location::ServerInfo& protoServerInfo)
{
    outServerInfo.name = QString::fromUtf8(protoServerInfo.name().c_str());
    outServerInfo.description = QString::fromUtf8(protoServerInfo.description().c_str());
    outServerInfo.host.setAddress(protoServerInfo.host());
    outServerInfo.port = static_cast<quint16>(protoServerInfo.port());
    outServerInfo.time.setTime_t(protoServerInfo.time());

    return outServerInfo;
}

inline LocationServiceClient::ServiceInfo& operator<<(
        LocationServiceClient::ServiceInfo& outServiceInfo,
        const ::protoqtrpc::location::ServiceInfo& protoServiceInfo)
{
    outServiceInfo.name = QString::fromUtf8(protoServiceInfo.name().c_str());
    outServiceInfo.description = QString::fromUtf8(protoServiceInfo.description().c_str());
    outServiceInfo.time.setTime_t(protoServiceInfo.time());

    return outServiceInfo;
}


template <typename OutType, typename InType>
QList<OutType>& operator<<(QList<OutType>& out,
                           const ::google::protobuf::RepeatedPtrField<InType>& in)
{
    OutType outType;

    out.reserve(in.size());

    for (typename ::google::protobuf::RepeatedPtrField<InType>::const_iterator it =
         in.begin(); it != in.end(); ++it)
    {
        outType << *it;
        out.push_back(outType);
    }
    return out;
}


inline LocationServiceClient::ServerServicesInfo& operator<<(
        LocationServiceClient::ServerServicesInfo& out,
        const ::protoqtrpc::location::ServerServicesInfo& in)
{
    out.serverInfo << in.serverinfo();
    out.servicesInfo << in.servicesinfo();

    return out;
}


inline LocationServiceClient::ServiceLocation& operator<<(
        LocationServiceClient::ServiceLocation& out,
        const ::protoqtrpc::location::ServiceLocation& in)
{
    out.serverInfo << in.serverinfo();
    out.serviceInfo << in.serviceinfo();

    return out;
}




LocationServiceClient::LocationServiceClient(const QHostAddress& host, quint16 port):
    d_ptr(new LocationServiceClientPrivate(
              host, port ? port : static_cast<quint16>(LOCATION_SERVICE_DEFAULT_PORT)))
{
    d_ptr->timerSession.setInterval(10000);
}

LocationServiceClient::~LocationServiceClient()
{
    delete d_ptr;
}

void LocationServiceClient::setSessionTimeout(int timeout)
{
    d_ptr->timerSession.setInterval(timeout);
}

int LocationServiceClient::getSessionTimeout() const
{
    return d_ptr->timerSession.interval();
}

const QHostAddress& LocationServiceClient::serverHost() const
{
    return d_ptr->channel.host();
}

quint16 LocationServiceClient::serverPort() const
{
    return d_ptr->channel.port();
}

LocationServiceClient::Status LocationServiceClient::registerServer(const QString& serverName,
                                                                    const QString& description,
                                                                    quint16& port)
{
    ServerRegistrationRequest request;
    ServerRegistrationResponse response;

    request.set_name(serverName.toUtf8().constData());
    request.set_description(description.toUtf8().constData());

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.registerServer(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to register the server [" << request.name().c_str()
                   << "]: " << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        if (response.status() != ::protoqtrpc::location::STATUS_ERROR_ALREADY_REGISTERED)
            qWarning() << "Failed to register the server [" << request.name().c_str()
                       << "]: " << Status_Name(response.status()).c_str();
        else
        {
            qWarning() << "Server [" << request.name().c_str()
                       << "] is already registered";
            port = static_cast<quint16>(response.port());
        }
        return static_cast<Status>(response.status());
    }

    port = static_cast<quint16>(response.port());

    return STATUS_SUCCESS;
}

LocationServiceClient::Status LocationServiceClient::unregisterServer(const QString& serverName)
{
    NameRequest request;
    GenericResponse response;

    request.set_name(serverName.toUtf8().constData());

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.unregisterServer(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to unregister the server [" << request.name().c_str()
                   << "]: " << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        qWarning() << "Failed to unregister the server [" << request.name().c_str()
                   << "]: " << Status_Name(response.status()).c_str();
        return static_cast<Status>(response.status());
    }

    return STATUS_SUCCESS;
}

LocationServiceClient::Status LocationServiceClient::getServerInfo(const QString& serverName,
                                                                   ServerInfo& serverInfo)
{
    NameRequest request;
    ServerInfoResponse response;

    request.set_name(serverName.toUtf8().constData());

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.getServerInfo(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to get information for a service [" << request.name().c_str()
                   << "]: " << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        qWarning() << "Failed to get information for a service [" << request.name().c_str()
                   << "]: " << Status_Name(response.status()).c_str();
        return static_cast<Status>(response.status());
    }

    serverInfo << response.serverinfo();

    return STATUS_SUCCESS;
}

LocationServiceClient::Status LocationServiceClient::getServerServicesInfo(const QString& serverName,
                                                                           ServerServicesInfo& info)
{
    NameRequest request;
    ServerServicesInfoResponse response;

    request.set_name(serverName.toUtf8().constData());

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.getServerServicesInfo(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to get information for a service [" << request.name().c_str()
                   << "]: " << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        qWarning() << "Failed to get information for a service [" << request.name().c_str()
                   << "]: " << Status_Name(response.status()).c_str();
        return static_cast<Status>(response.status());
    }

    info.serverInfo << response.serverinfo().serverinfo();
    info.servicesInfo << response.serverinfo().servicesinfo();

    return STATUS_SUCCESS;
}

LocationServiceClient::Status LocationServiceClient::getServersInfo(QList<ServerServicesInfo>& infos)
{
    VoidRequest request;
    ServersInfoResponse response;

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.getServersInfo(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to get information for services: "
                   << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        qWarning() << "Failed to get information for services: "
                   << Status_Name(response.status()).c_str();
        return static_cast<Status>(response.status());
    }

    infos << response.servers();

    return STATUS_SUCCESS;
}

LocationServiceClient::Status LocationServiceClient::registerService(const QString& serverName,
                                                                     const QString& serviceName,
                                                                     const QString& serviceDescription)
{
    ServiceRegistrationRequest request;
    GenericResponse response;

    request.set_servername(serverName.toUtf8().constData());
    request.set_servicename(serviceName.toUtf8().constData());
    request.set_description(serviceDescription.toUtf8().constData());

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.registerService(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to register the service [" << request.servicename().c_str()
                   << "] on the server [" << request.servername().c_str() << "]: "
                   << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        if (response.status() != ::protoqtrpc::location::STATUS_ERROR_ALREADY_REGISTERED)
            qWarning() << "Failed to register the service [" << request.servicename().c_str()
                       << "] on the server [" << request.servername().c_str() << "]: "
                       << Status_Name(response.status()).c_str();
        else
            qWarning() << "Service [" << request.servicename().c_str()
                       << "] is already registered";
        return static_cast<Status>(response.status());
    }

    return STATUS_SUCCESS;
}

LocationServiceClient::Status LocationServiceClient::getServiceInfo(const QString& serverName,
                                                                    const QString& serviceName,
                                                                    ServiceInfo& serviceInfo)
{
    ServiceId request;
    ServiceInfoResponse response;

    request.set_servername(serverName.toUtf8().constData());
    request.set_servicename(serviceName.toUtf8().constData());

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.getServiceInfo(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to find a service [" << request.servicename().c_str()
                   << "]: " << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        qWarning() << "Failed to find a service [" << request.servicename().c_str()
                   << "]: " << Status_Name(response.status()).c_str();
        return static_cast<Status>(response.status());
    }

    serviceInfo << response.info();

    return STATUS_SUCCESS;
}


LocationServiceClient::Status LocationServiceClient::locateService(const QString& serviceName,
                                                                   QList<ServiceLocation>& serviceLocations,
                                                                   const QHostAddress& serverHost,
                                                                   const QString& serverName)
{
    LocateServiceRequest request;
    LocateServiceResponse response;

    request.set_servicename(serviceName.toUtf8().constData());
    if (serverHost != QHostAddress::Any)
        request.set_serverhost(serverHost.toIPv4Address());
    if (!serverName.isEmpty())
        request.set_servername(serverName.toUtf8().constData());

    d_ptr->controller.Reset();
    d_ptr->closure.reset();
    d_ptr->client.locateService(&d_ptr->controller, &request, &response, &d_ptr->closure);
    d_ptr->closure.wait();

    if (d_ptr->controller.Failed())
    {
        qWarning() << "Failed to find a service [" << request.servicename().c_str()
                   << "]: " << d_ptr->controller.ErrorText().c_str();
        return STATUS_ERROR_OTHER;
    }

    if (response.status() != ::protoqtrpc::location::STATUS_SUCCESS)
    {
        qWarning() << "Failed to find a service [" << request.servicename().c_str()
                   << "]: " << Status_Name(response.status()).c_str();
        return static_cast<Status>(response.status());
    }

    serviceLocations << response.locations();

    return STATUS_SUCCESS;
}


}
}
